/*
 * Copyright 2012 Andrea Parodi
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package eban.storebyrules;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import eban.storebyrules.results.Result;
import eban.storebyrules.typemanagement.FieldTypeManager;
import sun.reflect.ReflectionFactory;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * Author: parroit
 * Created: 10/05/12 10.36
 */
public class RuntimeConverter<TBean> {

    //private final PersistentCollectionImpl<TBean> persistentCollection;

    public TBean buildNewObject(Result.Row rs) {
        TBean instance = null;

        try {

            Constructor constructor = getConstructor();

            instance = clazz.cast(constructor.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(PersistentCollectionImpl.CANNOT_INSTANTIATE_NEW_BEAN_INSTANCE, e);
        }



        Result.Columns columns = rs.getResult().columns();
        for (Field m : cache.getFields())
            setFieldValueFromRow(instance, rs, columns.byName(m.getName()));

        return instance;
    }


    private Constructor getConstructor() {

        Constructor constructor = null;
        try {
            constructor = clazz.getDeclaredConstructor();
        } catch (Exception e) {
        }
        if (constructor == null) {
            constructor = getFirstConstructorInHierarchy(clazz);
            ReflectionFactory rf = ReflectionFactory.getReflectionFactory();
            constructor = rf.newConstructorForSerialization(clazz, constructor);

        }

        return constructor;
    }

    private Constructor getFirstConstructorInHierarchy(Class<?> clazz1) {
        try {
            return clazz1.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            return getFirstConstructorInHierarchy(clazz1.getSuperclass());
        }

    }

    private final Class<TBean> clazz;
    public RuntimeConverter(Class<TBean> clazz) {
        Preconditions.checkNotNull(clazz);
        this.clazz = clazz;
        cache = new PrivateFieldsCache(clazz);
    }

    public ImmutableCollection<Field> persistentFields() {
        return cache.getFields();
    }

    public boolean isKey(Field field) {

        return cache.key.equals(field);
    }

    public String keyName() {

        return cache.key.getName();
    }

    public String engineKeyValue(TBean instance) {


        return FieldTypeManager.Registry.<Object>findManager((Class<Object>) cache.key.getType())
                .toEngineValue(keyValue(instance));


    }

    public Object keyValue(TBean instance)  {
        try {
            return cache.key.get(instance);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("cannot retrieve key");
        }
    }

    public Key keyAnnotation() {

        return cache.key.getAnnotation(Key.class);
    }

    public void setFieldValueFromRow(TBean instance, final Result.Row row, Result.Column column) {


        Field field = cache.getField(column.getName());

        Object value = row.get(column);
        Class<?> typeOfField = null;
        try {
            if (field.isAnnotationPresent(DynamicType.class))
                setFieldValueFromRow(instance, row, row.getResult().columns().byName(("clazz")));
            typeOfField = cache.getTypeOfField(instance, field.getName());
            Object runTimeValue = FieldTypeManager.Registry.findManager(typeOfField).toRuntimeValue(value);
            field.set(instance, runTimeValue);

        } catch (IllegalAccessException e) {
            throw new RuntimeException("cannot set field", e);
        }

    }


    private final PrivateFieldsCache cache;

    public Class<?> getTypeOfField(TBean instance, String name) {
        return cache.getTypeOfField(instance, name);
    }

    class PrivateFieldsCache {
        private final Field key;
        private final ImmutableMap<String, Field> fields;

        public Field getKey() {
            if (key == null) {
                throw new IllegalStateException("key field not present for class ");
            }
            return key;
        }

        public Class<?> getTypeOfField(TBean instance, String name) {
            Preconditions.checkNotNull(name);
            Class<?> typeOfField;
            Field field = getField(name);
            if (field.isAnnotationPresent(DynamicType.class)) {
                String factoryMethodName = field.getAnnotation(DynamicType.class).factory();

                try {
                    Method method = field.getDeclaringClass().getMethod(factoryMethodName);
                    method.setAccessible(true);
                    typeOfField = Class.class.cast(method.invoke(instance));
                } catch (Exception e) {
                    throw new PersistenceDesignException("cannot find field to use as type for field " + field.getName(), e);
                }
            } else
                typeOfField = field.getType();
            return typeOfField;
        }

        public ImmutableCollection<Field> getFields() {
            if (fields == null || fields.isEmpty()) {
                throw new IllegalStateException("fields map empty");
            }
            return fields.values();
        }

        public Field getField(String name) {
            Preconditions.checkNotNull(name, "name clazz");
            Field field = fields.get(name);
            if (field == null)
                throw new RuntimeException("field not found:" + name);
            return field;
        }

        PrivateFieldsCache(Class<?> clazz) {
            Preconditions.checkNotNull(clazz, "parameter clazz");
            Map<String, Field> buff = Maps.newHashMap();
            Optional<Field> keyBuff = Optional.absent();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(Persistent.class)) {
                    field.setAccessible(true);
                    buff.put(field.getName(), field);
                } else if (field.isAnnotationPresent(Key.class)) {
                    field.setAccessible(true);
                    buff.put(field.getName(), field);
                    keyBuff = Optional.of(field);
                }


            }
            if (!keyBuff.isPresent()) {
                throw new PersistenceDesignException("key field not found in class " + clazz.getName());
            }

            key = keyBuff.get();
            fields = ImmutableMap.copyOf(buff);


        }

    }


}
